# -*- coding: utf-8 -*-
'''
    >>> html = """
    ...   <?xml version="1.0" encoding="ISO-8859-1"?>
    ...    <bookstore>
    ...    <book category="COOKING">
    ...      <title lang="en">Everyday Italian</title>
    ...      <author>Giada De Laurentiis</author>
    ...      <year>2005</year>
    ...      <price>30.00</price>
    ...    </book>
    ...    <book category="CHILDREN">
    ...      <title lang="en">Harry Potter</title>
    ...      <author>J K. Rowling</author>
    ...      <year>2005</year>
    ...      <price>29.99</price>
    ...    </book>
    ...    <book category="WEB">
    ...      <title lang="en">XQuery Kick Start</title>
    ...      <author>James McGovern</author>
    ...      <author>Per Bothner</author>
    ...      <author>Kurt Cagle</author>
    ...      <author>James Linn</author>
    ...      <author>Vaidyanathan Nagarajan</author>
    ...      <year>2003</year>
    ...      <price>49.99</price>
    ...    </book>
    ...    <book category="WEB">
    ...      <title lang="en">Learning XML</title>
    ...      <author>Erik T. Ray</author>
    ...      <year>2003</year>
    ...      <price>39.95</price>
    ...    </book>
    ...    </bookstore>
    ... """

    >>> spoon = Spoon(html)
    >>> print spoon.bookstore.book[1].title
    Harry Potter
    
    >>> len(spoon.bookstore.book)
    4

    >>> for book in spoon.bookstore.book(category="WEB"):
    ...      print book.title
    XQuery Kick Start
    Learning XML
    
    >>> spoon._.author[4]
    <author>Kurt Cagle</author>
    
    >>> books = spoon.bookstore.book
    >>> print books(books.price > 40).title
    XQuery Kick Start
    
    >>> print (books.author == "Erik T. Ray")._parent.title
    Learning XML

    >>> len(books(books.year == 2003))
    2


    >>> print books(lambda souptag: souptag["category"] == "COOKING").title
    Everyday Italian


    >>> query = LazySpoon().bookstore.book[0].author.inner_html()
    >>> print "\\n".join([str(c) for c in query.__calls__])
    (<built-in function getattr>, ('bookstore',), {})
    (<built-in function getattr>, ('book',), {})
    (<built-in function getitem>, (0,), {})
    (<built-in function getattr>, ('author',), {})
    (<built-in function getattr>, ('inner_html',), {})
    (<built-in function apply>, (), {})

    >>> query.evaluate(spoon)
    'Giada De Laurentiis'


'''

from BeautifulSoup import BeautifulSoup
import operator
import re


__all__ = [
    "Spoon"
    ]


class LazySpoon(object):


    def __init__(self, obj=None):
        self.__obj__ = obj
        self.__calls__ = []


    def __call__(self, *args, **kwargs):
        self.__calls__.append((apply, args, kwargs))
        return self


    def __getitem__(self, key):
        self.__calls__.append((operator.getitem, (key,), {}))
        return self


    def __getattr__(self, name):
        self.__calls__.append((getattr, (name,), {}))
        return self


    def __getslice__(self, *slc):
        self.__calls__.append((operator.getslice, slc, {}))
        return self


    def evaluate(self, obj=None):
        obj = obj or self.__obj__
        for call, args, kwargs in self.__calls__:
            obj = call(obj, *args, **kwargs)
        return obj

    
    def __float__(self):
        return float(self.evaluate())


    def __int__(self):
        return int(self.evaluate())


    def __len__(self):
        return len(self.evaluate())


    def __str__(self):
        return str(self.evaluate())


    def __repr__(self):
        return repr(self.evaluate())


    def __iter__(self):
        return iter(self.evaluate())




op_map = {}
for op in "eq ne lt le gt ge".split():
    op_map[op] = getattr(operator, "__%s__" % op)
op_map["startswith"] = lambda s, w: s.startswith(w)
op_map["istartswith"] = lambda s, w: s.lower().startswith(w.lower())
op_map["endswith"] = lambda s, w: s.endswith(w)
op_map["iendswith"] = lambda s, w: s.lower().endswith(w.lower())
op_map["matches"] = lambda s, pat: re.match(pat, s) != None



class Spoon(object):


    def __init__(self, *soup):
        self._soup = list(soup)
        for i, s in enumerate(soup):
            if isinstance(s, basestring):
                self._soup[i] = BeautifulSoup(s)
            elif isinstance(s, file):
                self._soup[i] = BeautifulSoup(s.read())


    def __eq__(self, other): return self._op("eq", other)
    def __ne__(self, other): return self._op("ne", other)
    def __gt__(self, other): return self._op("gt", other)
    def __ge__(self, other): return self._op("ge", other)
    def __lt__(self, other): return self._op("lt", other)
    def __le__(self, other): return self._op("le", other)


    def _op(self, op, other):
        soups = []
        for s in self._soup:
            value = s.renderContents()
            if isinstance(other, float) or isinstance(other, int):
                value = float(value)
            if op_map[op](value, other):
                soups.append(s)
        return Spoon(*soups)
            

    def _find_tag(self, name):
        soups = []
        if name == "_parent":
            for s in self._soup:
                soups.append(s.parent)
        elif name == "_":
            for s in self._soup:
                soups.extend(s.findAll())
        else:
            for s in self._soup:
                found = s.findAll(name, recursive=False)
                if found:
                    soups.extend(found)
        return Spoon(*soups)


    def _find_attributes(self, attr):
        for s in self._soup:
            found = s.get(attr)
            if found:
                yield found


    def _find_attr(self, name):
        try:
            return self._find_attributes(name).next()
        except StopIteration:
            return u""


    def __setitem__(self, key, value):
        if isinstance(key, int):
            self._soup[key].string = value
        else:
            if key.startswith(u"@"):
                key = key[1:]
                for s in self._soup:
                    s[key] = value
            else:
                for s in self._soup:
                    setattr(s, key, value)

        
    def __getitem__(self, name):
        if isinstance(name, int):
            return Spoon(self._soup[name])
        if name.startswith(u"@"):
            return u"".join(list(self._find_attributes((name[1:]))))
        else:
            return self._find_tag(name)


    def __getattr__(self, name):
        if name.endswith("Tag"):
            name = name[:-3]
        return self._find_tag(name)


    def inner_html(self):
        return "".join([s.renderContents() for s in self._soup])


    def outer_html(self):
        return u"".join([unicode(s) for s in self._soup])


    def delete(self):
        self["hidden"] = True
        self["renderContents"] = lambda *args: ""


    def __repr__(self):
        return self.outer_html()


    def __str__(self):
        return self.inner_html()


    def __call__(self, *args, **kwargs):
        soups = []
        for s in self._soup:
            s.__getitem__ = s.get
            failed = False
            for a in args:
                if isinstance(a, Spoon):
                    for avs in a._soup:
                        av = unicode(avs)
                        svlist = [unicode(sv) for sv in s.findAll(recursive=False)]
                        failed = not(av in svlist)
                elif callable(a):
                    try:
                        result = a(s)
                    except:
                        result = None
                    if not result:
                        failed = True
                        break
                else:
                    raise TypeError, "%r is not callable" % a
    
            if not failed:
                for key, value in kwargs.items():
                    key = key.rsplit("__", 2)
                    name = key[0]
                    if len(key) == 1:
                        op = op_map["eq"]
                    else:
                        try:
                            op = op_map[key[1]]
                        except KeyError:
                            raise TypeError, "Unknown comparison %r for %r" % (
                                                                   key[1], name)
                    if name == "_":
                        found = False
                        for attr in s._getAttrMap().values():
                            if op(attr, value):
                                found = True
                                break
                        if not found:
                            failed = True
                            break
                    else:
                        if name.endswith("_"):
                            name = name[:-1]
                        attr = s.get(name)
                        if not attr or not op(attr, value):
                            failed = True
                            break
            if not failed:
                soups.append(s)
        return Spoon(*soups)


    def __iter__(self):
        for s in self._soup:
            yield Spoon(s)


    def __len__(self):
        return len(self._soup)



if __name__ == "__main__":
    import doctest
    doctest.testmod()
